#define _FILE_OFFSET_BITS 64

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdint.h>
#include <sys/types.h>
#include <errno.h>
#include <aio.h>

#include "Util.h"

#define NUM_SEQUENTIAL_READS 5
#define BLOCK_SIZE 4096

typedef enum {
	SEQUENTIAL, RANDOMOFFSET_SEQUENTIAL, RANDOM, ASYNC_SEQUENTIAL , ASYNC_RANDOMOFFSET_SEQUENTIAL, ASYNC_RANDOM
} readType_t;

struct workLoadParams {
	int option;
	int fileDescriptor;
	long fileSize;
	long numReads;
	char* fileName; // For display purpose
};

void doSequentialReads(int fileDescriptor, long numReads, long fileSize);
void doAsyncSequentialReads(int fileDescriptor, long numReads, long fileSize);
void doRandOffsetAndSequentialReads(int fileDescriptor, long numReads, long fileSize);
void doAsyncRandOffsetAndSequentialReads(int fileDescriptor, long numReads, long fileSize);
void doRandomReads(int fileDescriptor, long numReads, long fileSize);
void doAsyncRandomReads(int fileDescriptor, long numReads, long fileSize);

void* runWorkload(void* ptr);
struct workLoadParams* constructWorkLoadParams(int option, int fileDescriptor, long fileSize, long numReads, char* fileName);

long getRandomNumber();
void readBlocks(int fileDescriptor, long offset);

int main(int argc, char *argv[]) {
	long fileSize1, fileSize2, numReads;
	int option = 0, fileDescriptor1 = 0, fileDescriptor2 = 0;
	char *fileName1 = NULL, *fileName2 = NULL;
	readType_t readType;
    struct workLoadParams *params1, *params2;

    pthread_t readThread1, readThread2, readThread3, readThread4;

    validateAndFillInputArgumentsForFileSystemWorkload(argc, argv, &fileName1,
			"FileName1", &fileName2, "FileName2" , &numReads, "NumReads", &option, "Option");
	readType = option;
	srand(time(NULL ));

	fileDescriptor1 = open(fileName1, O_RDONLY);
	fileDescriptor2 = open(fileName2, O_RDONLY);
    if(fileDescriptor1 == -1 || fileDescriptor2 == -1) {
    	printf("Unable to open file %s or %s\n", fileName1, fileName2);
    	perror("Checking the error");
    	exit(-1);
    }

    printf("Filename1 :%s\n", fileName1);
    printf("Filename2 :%s\n", fileName2);

    fileSize1 = getFileSizeFromFD(fileDescriptor1);
    fileSize2 = getFileSizeFromFD(fileDescriptor2);
    printf("Filesize1 :%lu\t Filesize2 :%lu\n", fileSize1, fileSize2);

    params1 = constructWorkLoadParams(option, fileDescriptor1, fileSize1, numReads, fileName1);
    params2 = constructWorkLoadParams(option, fileDescriptor2, fileSize2, numReads, fileName2);

	pthread_create( &readThread1, NULL, runWorkload, (void*) params1);
	pthread_create( &readThread2, NULL, runWorkload, (void*) params2);
	pthread_create( &readThread3, NULL, runWorkload, (void*) params1);
	pthread_create( &readThread4, NULL, runWorkload, (void*) params2);


	pthread_join( readThread1, NULL);
	pthread_join( readThread2, NULL);
	pthread_join( readThread3, NULL);
	pthread_join( readThread4, NULL);

	return EXIT_SUCCESS;
}

void* runWorkload(void* ptr) {
    double timeForReads;
    struct timespec startTime, endTime;
    struct workLoadParams* param = (struct workLoadParams*) ptr;

    readType_t readType = param->option;
    int fileDescriptor = param->fileDescriptor;
    long fileSize = param->fileSize;
    long numReads = param->numReads;

	clock_gettime(CLOCK_REALTIME, &startTime);
	switch (readType) {
	case SEQUENTIAL:
		doSequentialReads(fileDescriptor, numReads, fileSize);
		break;
	case RANDOMOFFSET_SEQUENTIAL:
		doRandOffsetAndSequentialReads(fileDescriptor, numReads, fileSize);
		break;
	case RANDOM:
		doRandomReads(fileDescriptor, numReads, fileSize);
		break;
	case ASYNC_SEQUENTIAL:
		doAsyncSequentialReads(fileDescriptor, numReads, fileSize);
		break;
	case ASYNC_RANDOMOFFSET_SEQUENTIAL:
		doAsyncRandOffsetAndSequentialReads(fileDescriptor, numReads, fileSize);
		break;
	case ASYNC_RANDOM:
		doAsyncRandomReads(fileDescriptor, numReads, fileSize);
		break;
	default:
		printf("ERROR: ReadType is %d\n", readType);
		exit(-1);
	}
	clock_gettime(CLOCK_REALTIME, &endTime);
	timeForReads = getDiffInNanoSeconds(startTime, endTime);
	timeForReads /= 10e6;

	//printf("%lu %f\n", numReads,  (double) timeForReads / (double) numReads);
	printf("%s %lu %f\n", param->fileName, numReads,  (double) timeForReads);
	return NULL;
}

struct workLoadParams* constructWorkLoadParams(int option, int fileDescriptor, long fileSize, long numReads, char* fileName) {
	struct workLoadParams *param = (struct workLoadParams*) malloc(sizeof(struct workLoadParams));
	param->option = option;
	param->fileDescriptor = fileDescriptor;
	param->fileSize = fileSize;
	param->numReads = numReads;
	param->fileName = fileName;
	printf("Option: %d \tFileDescriptor: %d \tFileSize :%lu \t numReads :%lu fileName :%s\n", option, fileDescriptor, fileSize, numReads, fileName);
	return param;
}

void doSequentialReads(int fileDescriptor, long numReads, long fileSize) {
	unsigned long offset = 0;
    int counter = 0;
	for(counter = 0; counter < numReads ; counter++ ) {
		//offset = (unsigned long) getRandomNumber();
		readBlocks(fileDescriptor, offset);
		offset += BLOCK_SIZE;
	}
}

void doRandOffsetAndSequentialReads(int fileDescriptor, long numReads, long fileSize) {
	unsigned long offset = 0;
    int randCounter = 0, numRandOffsets = numReads / NUM_SEQUENTIAL_READS, seqCounter;
	for(randCounter = 0; randCounter < numRandOffsets ; randCounter++ ) {
		offset = (unsigned long) getRandomNumber();
		for(seqCounter = 0; seqCounter < NUM_SEQUENTIAL_READS; seqCounter++) {
			readBlocks(fileDescriptor, offset);
			offset += BLOCK_SIZE;
		}
	}
}

void doAsyncRandOffsetAndSequentialReads(int fileDescriptor, long numReads, long fileSize) {
	unsigned long offset = 0;
    int randCounter = 0, numRandOffsets = numReads / NUM_SEQUENTIAL_READS, seqCounter, counter, numBytesRead;
	struct aiocb cb[numReads];
	char buffer[BLOCK_SIZE]; // I know what I am doing

	// Enqueue
	for(randCounter = 0; randCounter < numRandOffsets ; randCounter++ ) {
		offset = (unsigned long) getRandomNumber();
		for(seqCounter = 0; seqCounter < NUM_SEQUENTIAL_READS; seqCounter++) {
			readBlocks(fileDescriptor, offset);
            counter = randCounter *  NUM_SEQUENTIAL_READS + seqCounter;

			memset(&cb[counter], 0, sizeof(struct aiocb));
			cb[counter].aio_nbytes = BLOCK_SIZE;
			cb[counter].aio_fildes = fileDescriptor;
			cb[counter].aio_offset = offset;
			cb[counter].aio_buf = buffer;

			if (aio_read(&cb[counter]) == -1) {
				printf("Unable to create aio request at offset %lu\n", offset);
				perror("ERROR in AIO READ!");
				exit(-1);
			}
			offset += BLOCK_SIZE;
		}
	}

	// Read
	for (counter = 0; counter < numReads; counter++) {
		while (aio_error(&cb[counter]) == EINPROGRESS) {
			;
		}
		numBytesRead = aio_return(&cb[counter]);
		if (numBytesRead == -1) {
			perror("Error in aio request");
			exit(-1);
		}
	}

}


void doRandomReads(int fileDescriptor, long numReads, long fileSize) {
	unsigned long offset = 0;
    int counter = 0;
	for(counter = 0; counter < numReads ; counter++ ) {
		offset = (unsigned long) getRandomNumber();
		readBlocks(fileDescriptor, offset);
	}
}

void doAsyncRandomReads(int fileDescriptor, long numReads, long fileSize) {
	unsigned long offset = 0;
	int counter = 0, numBytesRead = 0;
	struct aiocb cb[numReads];
	char buffer[BLOCK_SIZE]; // I know what I am doing

	// Enqueue
	for (counter = 0; counter < numReads; counter++) {
		offset = (unsigned long) getRandomNumber();

		memset(&cb[counter], 0, sizeof(struct aiocb));
		cb[counter].aio_nbytes = BLOCK_SIZE;
		cb[counter].aio_fildes = fileDescriptor;
		cb[counter].aio_offset = offset;
		cb[counter].aio_buf = buffer;

		if (aio_read(&cb[counter]) == -1) {
			printf("Unable to create aio request at offset %lu\n", offset);
			perror("ERROR in AIO READ!");
			exit(-1);
		}

	}

	// Read
	for (counter = 0; counter < numReads; counter++) {
		while (aio_error(&cb[counter]) == EINPROGRESS) {
			;
		}
		numBytesRead = aio_return(&cb[counter]);
		if (numBytesRead == -1) {
			perror("Error in aio request");
			exit(-1);
		}
	}

}

void doAsyncSequentialReads(int fileDescriptor, long numReads, long fileSize) {
	unsigned long offset = 0;
	int counter = 0, numBytesRead = 0;
	struct aiocb cb[numReads];
	char buffer[BLOCK_SIZE]; // I know what I am doing

	offset = (unsigned long) getRandomNumber();

	// Enqueue
	for (counter = 0; counter < numReads; counter++) {

		memset(&cb[counter], 0, sizeof(struct aiocb));
		cb[counter].aio_nbytes = BLOCK_SIZE;
		cb[counter].aio_fildes = fileDescriptor;
		cb[counter].aio_offset = offset;
		cb[counter].aio_buf = buffer;

		if (aio_read(&cb[counter]) == -1) {
			printf("Unable to create aio request at offset %lu\n", offset);
			perror("ERROR in AIO READ!");
			exit(-1);
		}

		offset += BLOCK_SIZE;
	}

	// Read
	for (counter = 0; counter < numReads; counter++) {
		while (aio_error(&cb[counter]) == EINPROGRESS) {
			;
		}
		numBytesRead = aio_return(&cb[counter]);
		if (numBytesRead == -1) {
			perror("Error in aio request");
			exit(-1);
		}
	}

}

void readBlocks(int fileDescriptor, long offset) {
	char buffer[BLOCK_SIZE];
	int readResult, lseekResult;
	lseekResult = lseek(fileDescriptor, offset, SEEK_SET);
	if(lseekResult == -1) {
		printf("Unable to seek to offset %lu\n", offset);
		exit(-1);
	}
	readResult = read(fileDescriptor, buffer, BLOCK_SIZE);
    if(readResult == -1) {
    	printf("Unable to read block from location %lu of file %d\n", offset, fileDescriptor);
    	exit(-1);
    }
    //printf("Read a block from offset %lu and size %d\n", offset, readResult);
}

long getRandomNumber() {
	uint64_t random =
	  (((uint64_t) rand() <<  0) & 0x00000000FFFFFFFFull) |
	  (((uint64_t) rand() << 32) & 0xFFFFFFFF00000000ull);
	return (long) random;
}
